// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This is an example binary to exercise ulib/tftp. It runs on Linux or macOS.

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "tftp/tftp.h"

#define BLOCKSZ 1024
#define WINSZ   64

#define DROPRATE 100

#define SCRATCHSZ 2048
static char scratch[SCRATCHSZ];
static char out_scratch[SCRATCHSZ];
static char in_scratch[SCRATCHSZ];

typedef struct connection connection_t;

struct tftp_file {
    int fd;
    size_t size;
};

struct connection {
    int socket;
    struct sockaddr_in out_addr;
    struct sockaddr_in in_addr;
    uint32_t previous_timeout_ms;
};

tftp_status connection_send(void* data, size_t len, void* transport_cookie) {
    connection_t* connection = (connection_t*)transport_cookie;
#if DROPRATE != 0
    if (rand() % DROPRATE == 0) {
        fprintf(stderr, "DROP\n");
        return len;
    }
#endif
    uint8_t* msg = data;
    uint16_t opcode = ntohs(*(uint16_t*)msg);
    fprintf(stderr, "sending opcode=%u\n", opcode);
    ssize_t send_result = sendto(connection->socket, data, len, 0, (struct sockaddr*)&connection->out_addr,
                                 sizeof(struct sockaddr_in));
    if (send_result != len) {
        return TFTP_ERR_IO;
    }
    return TFTP_NO_ERROR;
}

int connection_receive(void* data, size_t len, bool block, void* transport_cookie) {
    connection_t* connection = (connection_t*)transport_cookie;
    socklen_t server_len;
    int fl = fcntl(connection->socket, F_GETFL, 0);
    if (fl < 0) {
        int e = errno;
        fprintf(stderr, "could not get socket flags: %d\n", errno);
        errno = e;
        return -1;
    }
    if (block) {
        fl &= ~O_NONBLOCK;
    } else {
        fl |= O_NONBLOCK;
    }
    int ret = fcntl(connection->socket, F_SETFL, fl);
    if (ret < 0) {
        int e = errno;
        fprintf(stderr, "could not set socket flags: %d\n", errno);
        errno = e;
        return -1;
    }
    ssize_t recv_result = recvfrom(connection->socket, data, len, 0,
                                   (struct sockaddr*)&connection->in_addr,
                                   &server_len);
    if (recv_result < 0) {
        if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
            return TFTP_ERR_TIMED_OUT;
        fprintf(stderr, "failed during recvfrom: errno=%d\n", (int) errno);
        return TFTP_ERR_INTERNAL;
    }
    return recv_result;
}

int connection_set_timeout(uint32_t timeout_ms, void* transport_cookie) {
    connection_t* connection = (connection_t*)transport_cookie;
    if (connection->previous_timeout_ms != timeout_ms && timeout_ms > 0) {
        fprintf(stdout, "Setting timeout to %dms\n", timeout_ms);
        connection->previous_timeout_ms = timeout_ms;
        struct timeval tv;
        tv.tv_sec = timeout_ms / 1000;
        tv.tv_usec = 1000 * (timeout_ms - 1000 * tv.tv_sec);
        return setsockopt(connection->socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    }
    return 0;
}

connection_t* create_connection(const char* hostname, int incoming_port, int outgoing_port) {
    connection_t* connection = (connection_t*)malloc(sizeof(connection_t));
    memset(connection, 0, sizeof(connection_t));

    struct hostent* server;

    if ((connection->socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        fprintf(stderr, "Cannot create socket\n");
        goto err;
    }

    if (!(server = gethostbyname(hostname))) {
        fprintf(stderr, "Could not resolve host '%s'\n", hostname);
        goto err;
    }

    memset(&connection->out_addr, 0, sizeof(struct sockaddr_in));
    connection->out_addr.sin_family = AF_INET;
    connection->out_addr.sin_port = htons(outgoing_port);
    void* server_addr = server->h_addr_list[0];
    memcpy(&connection->out_addr.sin_addr.s_addr, server_addr, server->h_length);

    memset(&connection->in_addr, 0, sizeof(struct sockaddr_in));
    connection->in_addr.sin_family = AF_INET;
    connection->in_addr.sin_port = htons(incoming_port);
    memcpy(&connection->in_addr.sin_addr.s_addr, server_addr, server->h_length);
    if (bind(connection->socket, (struct sockaddr*)&connection->in_addr, sizeof(struct sockaddr_in)) == -1) {
        fprintf(stderr, "Could not bind\n");
        goto err;
    }

    connection->previous_timeout_ms = 0;
    return connection;

err:
    if (connection->socket) close(connection->socket);
    free(connection);
    return NULL;
}

void print_usage(void) {
    fprintf(stdout, "tftp (-s filename|-r)\n");
    fprintf(stdout, "\t -s filename to send the provided file\n");
    fprintf(stdout, "\t -r to receive a file\n");
}

ssize_t open_read_file(const char* filename, void* file_cookie) {
    fprintf(stdout, "Opening %s for reading\n", filename);
    int fd = open(filename, O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "could not open file: err=%d\n", errno);
        return TFTP_ERR_IO;
    }
    struct tftp_file* f = (struct tftp_file*)file_cookie;
    f->fd = fd;
    struct stat st;
    if (fstat(f->fd, &st) < 0) {
        fprintf(stderr, "could not get file size: err=%d\n", errno);
        return TFTP_ERR_IO;
    }
    return st.st_size;
}

tftp_status open_write_file(const char* filename, size_t size, void* file_cookie) {
    fprintf(stdout, "Opening %s for writing\n", filename);
    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
                            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (fd < 0) {
        fprintf(stderr, "could not open file: err=%d\n", errno);
        return TFTP_ERR_IO;
    }
    struct tftp_file* f = (struct tftp_file*)file_cookie;
    f->fd = fd;
    return TFTP_NO_ERROR;
}

tftp_status read_file(void* data, size_t* length, off_t offset, void* file_cookie) {
    int fd = ((struct tftp_file*)file_cookie)->fd;
    ssize_t n = pread(fd, data, *length, offset);
    if (n < 0) {
        fprintf(stderr, "could not read file: offset %jd, err=%d\n", (intmax_t)offset, errno);
        return n;
    }
    *length = n;
    return TFTP_NO_ERROR;
}

tftp_status write_file(const void* data, size_t* length, off_t offset, void* file_cookie) {
    struct tftp_file* f = file_cookie;
    int fd = f->fd;
    ssize_t n = pwrite(fd, data, *length, offset);
    if (n < 0) {
        fprintf(stderr, "could not write file: offset %jd, err=%d\n", (intmax_t)offset, errno);
        return n;
    }
    *length = n;
    f->size = offset + *length;
    return TFTP_NO_ERROR;
}

void close_file(void* file_cookie) {
    struct tftp_file* f = file_cookie;
    close(f->fd);
}

int tftp_send_file_wrapper(tftp_session* session,
                           connection_t* connection,
                           const char* filename) {
    uint16_t block_size = BLOCKSZ;
    uint16_t window_size = WINSZ;
    tftp_set_options(session, &block_size, NULL, &window_size);

    struct tftp_file file_cookie;
    char err_msg[128];
    tftp_request_opts options = { 0 };
    options.inbuf = in_scratch;
    options.inbuf_sz = sizeof(in_scratch);
    options.outbuf = out_scratch;
    options.outbuf_sz = sizeof(out_scratch);
    options.err_msg = err_msg;
    options.err_msg_sz = sizeof(err_msg);
    tftp_status send_result =
        tftp_push_file(session, connection, &file_cookie, filename,
                       "zircon.bin", &options);
    if (send_result == TFTP_NO_ERROR) {
        return 0;
    }
    fprintf(stderr, "%s\n", err_msg);
    return -1;
}

int tftp_receive_file_wrapper(tftp_session* session,
                              connection_t* connection) {
    struct tftp_file file_cookie;
    char err_msg[128];
    tftp_handler_opts options = { 0 };
    options.inbuf = in_scratch;
    options.inbuf_sz = sizeof(in_scratch);
    options.outbuf = out_scratch;
    size_t outbuf_sz = sizeof(out_scratch);
    options.outbuf_sz = &outbuf_sz;
    options.err_msg = err_msg;
    options.err_msg_sz = sizeof(err_msg);

    tftp_status status;
    do {
        status = tftp_service_request(session, connection, &file_cookie,
                                     &options);
    } while (status == TFTP_NO_ERROR || status == TFTP_ERR_TIMED_OUT);
    if (status == TFTP_TRANSFER_COMPLETED)
        return 0;
    fprintf(stderr, "%s\n", err_msg);
    return 1;
}

int main(int argc, char* argv[]) {
    const char* hostname = "127.0.0.1";
    int port = 2343;

    srand(time(NULL));

    if (argc < 2) {
        print_usage();
        return 1;
    }

    tftp_session* session = NULL;
    if (SCRATCHSZ < tftp_sizeof_session()) {
        fprintf(stderr, "Need more space for tftp session: %d < %zu\n",
                SCRATCHSZ, tftp_sizeof_session());
        return -1;
    }
    if (tftp_init(&session, scratch, SCRATCHSZ)) {
        fprintf(stderr, "Failed to initialize TFTP Session\n");
        return -1;
    }

    tftp_file_interface file_interface = {open_read_file,
                                          open_write_file,
                                          read_file,
                                          write_file,
                                          close_file};
    tftp_session_set_file_interface(session, &file_interface);
    tftp_transport_interface transport_interface = {connection_send,
                                                    connection_receive,
                                                    connection_set_timeout};
    tftp_session_set_transport_interface(session, &transport_interface);

    connection_t* connection;
    if (!strncmp(argv[1], "-s", 2)) {
        if (argc < 3) {
            print_usage();
            return 1;
        }
        connection = create_connection(hostname, port, port + 1);
        if (!connection) {
            return -1;
        }
        return tftp_send_file_wrapper(session, connection, argv[2]);
    } else if (!strncmp(argv[1], "-r", 2)) {
        connection = create_connection(hostname, port + 1, port);
        if (!connection) {
            return -1;
        }
        return tftp_receive_file_wrapper(session, connection);
    } else {
        print_usage();
        return 2;
    }
    return 0;
}
